結論先說:光有實驗設計還不夠,得真的跑過一次、把 metrics、logs、traces 兜成同一條因果鏈,並讓每個 scenario 的預期結果寫清楚,才算完成一次可被檢視的 chaos experiment。
承接上文:上篇定義了合格實驗的骨架(hypothesis → steady state → injection → observe → rollback → learn),列出 AI workflow 專屬的注入點,寫了三份 DIY experiment card(provider timeout、DB unavailable、tool provider failure),並用 Cloudflare、Knight Capital、Meta 三個真實事故,說明安全欄杆與 kill switch 為什麼要設計成不依賴被測系統本身。下篇接著把這些設計真的跑一次,並把發現收斂成長期可靠性工作。
Day 17、Day 18 和 Day 20 分別把 metrics、traces、logs 和 alert 拉進 SRE Lab。
今天的最低要求是:每一個注入 request 都能由 request_id 找到四種證據。
request_id
├─ HTTP access log
├─ root trace and child spans
├─ outcome counter increment
└─ experiment evidence record
不要把 request_id 放進 Prometheus label。
它的 cardinality 會失控。
metrics 只保留低 cardinality 的分類,例如 scenario、outcome、dependency 和 environment。
ASK_OUTCOMES.labels(
workflow="ask",
outcome=result.workflow_status,
degraded=str(result.degraded).lower(),
).inc()
DEPENDENCY_FAILURES.labels(
dependency="model_provider",
failure_class="timeout",
).inc()
scenario 是否放進 metric label,要看你是否會無限制新增 scenario。
如果實驗 ID 每次都不同,就不要當 label。
把 experiment ID 放在 structured log、trace attribute 或 evidence file。
以下是建議的結構化 log。
logger.info(
"ask workflow finished",
extra={
"request_id": request_id,
"experiment_id": "ce-ask-provider-timeout-v1",
"chaos_scenario": chaos.scenario,
"technical_status": result.technical_status,
"workflow_status": result.workflow_status,
"task_status": result.task_status,
"degraded": result.degraded,
"prompt_version": prompt_version,
"deployment_version": deployment_version,
},
)
不要把完整 prompt、retrieved document 或 API key 一起記進去。
需要重現時,優先記錄版本、文件識別字、長度、hash 或已遮罩的分類欄位。
trace 的 attribute 也遵守同樣原則。
with tracer.start_as_current_span("model.generate") as span:
span.set_attribute("ai.workflow.name", "ask")
span.set_attribute("ai.chaos.scenario", chaos.scenario)
span.set_attribute("ai.provider.name", "fake-provider")
span.set_attribute("ai.prompt.version", prompt_version)
span.set_attribute("ai.retrieval.index_version", index_version)
觀測不是「把所有東西都記下來」。
它是讓你以最少的敏感資料,還原足以做決策的因果鏈。
前面十段講了骨架、注入點、experiment card、guardrail 與 observability,接下來把它們接成一條完整的迴路。這個迴路和上篇 ① 段那行 hypothesis → steady state → controlled failure injection → observe → rollback → learn 是同一件事,只是攤開成八個可以逐步執行的步驟。
Step 1 填 experiment card ──────────┐
Step 2 取得 baseline │ 這四步都還沒碰 chaos 開關
Step 3 確認 abort 開關真的能關 │ (對照上篇 ⑦ 的 Meta 案例:
Step 4 只對一筆 selected request 注入 timeout ── 注入開始
Step 5 查 metrics │ 這三步是 observe
Step 6 查 log 和 trace │
Step 7 停止 injection 後再跑 baseline ── rollback
Step 8 填寫 actual result ── learn,回到上篇 ⑥ card
前四步的順序刻意設計成「先確認能不能安全地停,再開始弄壞東西」——這正是上篇 ⑦ 段 Meta Oct 2021 案例要提醒的:如果 Step 3 沒有先驗證過 abort 開關不依賴被測系統本身,Step 4 一旦出狀況,你可能會發現自己正在複製一次小規模的「復原路徑跟著故障系統一起斷線」。
以下步驟由讀者在自己的隔離環境執行。
本文不會建立 DIY/、安裝 dependency、啟動 container、呼叫 provider 或宣稱驗證結果。
開始前,確認你具備以下條件。
把上一節的 experiment-card.yaml 複製到自己的工作目錄。
換掉 owner、service name、預期門檻與 rollback action。
確認 environment 不是 production。
確認 synthetic tenant 由程式拒絕其他 tenant 的機制保護,不能只靠人記得。
請另一位 reviewer 看過 abort condition。
預期觀測:此時不應有任何 request、metric 或 trace 因為寫文件而改變。
驗收:card 能在不口頭補充的情況下,讓 reviewer 說出何時開始、何時停止、什麼結果算失敗。
先以沒有 X-Chaos-Scenario 的 request 跑一筆合成問題。
範例 command 依你的 service address 調整。
curl --fail-with-body \
-H 'X-Tenant: synthetic-lab' \
-H 'Content-Type: application/json' \
--data '{"question":"What is the synthetic remote-work policy?"}' \
http://127.0.0.1:8000/ask
預期觀測:request 完成,workflow status 與 task status 符合你的 baseline contract。
預期觀測:log 有 request_id,trace 有 root span,metric 只有正常 outcome 增加。
驗收:你能用同一個 request_id 找到 log 與 trace;若找不到,先修 observability,不要繼續注入。
在注入前先確認你知道如何移除 header、停用 fake adapter 或關閉測試環境。
把實際操作寫進 card 的 rollback.action。
不要只寫「必要時 rollback」。
「必要時」不是 command。
預期觀測:此步不應改變 workflow outcome。
驗收:另一位 reviewer 只看文件就能描述恢復 baseline 的動作。
curl --fail-with-body \
-H 'X-Tenant: synthetic-lab' \
-H 'X-Chaos-Scenario: provider-timeout' \
-H 'Content-Type: application/json' \
--data '{"question":"What is the synthetic remote-work policy?"}' \
http://127.0.0.1:8000/ask
預期觀測:fake provider 在 selected request raise ProviderTimeout。
預期觀測:workflow 回 provider_unavailable 或你已宣告的安全降級狀態。
預期觀測:沒有 tool action。
預期觀測:dependency failure metric 增加一次。
預期觀測:trace 顯示 provider child span failed,root span 保留 request_id 與 outcome。
驗收:如果 API 回 200,但 task_status=completed,實驗應判定失敗;這代表結果分類在降級路徑被寫壞。
最少對照下列訊號。
ask_requests_total
ask_outcomes_total{workflow_status="provider_unavailable"}
dependency_failures_total{dependency="model_provider",failure_class="timeout"}
tool_action_attempted_total
circuit_breaker_state
預期觀測:timeout 類 failure 增加。
預期觀測:工具 action counter 沒有增加。
預期觀測:若已超過 breaker 門檻,breaker state 轉為預期狀態;若沒有超過,不能硬說「breaker 沒作用」。
驗收:metrics 數字與 request 數量、retry policy 和實驗 card 相符。
從 selected request 的 request_id 開始。
確認你能依序看到:request accepted、retrieval、provider timeout、fallback decision、response emitted。
如果你的 workflow 沒有 retrieval 或 fallback,順序可不同。
重點是因果關係要可見,不是 span 名稱一定要長得一樣。
預期觀測:failure class 與 card 的 scenario 一致。
預期觀測:trace attribute 有版本資訊,但沒有未遮罩的 prompt 或 credential。
驗收:沒有靠猜測就能說明為何 task 沒完成、是否執行 tool、由哪個版本處理。
移除 X-Chaos-Scenario。
重新送一筆合成 baseline request。
預期觀測:service 回到原本 behavior。
預期觀測:不會因為測試留下全域 timeout、open breaker 或被污染的 fake state。
驗收:恢復後 baseline 和 Step 2 的 outcome 相容;如果不相容,先記錄為實驗副作用,暫停下一個 scenario。
card 的結果區可長這樣。
result:
started_at: "2026-09-22T10:00:00+08:00"
stopped_at: "2026-09-22T10:04:00+08:00"
selected_requests: 1
expected:
task_status: not_completed
tool_action_attempted_delta: 0
actual:
task_status: not_completed
tool_action_attempted_delta: 0
dependency_timeout_delta: 1
decision: passed
follow_up: "add regression test for provider timeout classification"
實際結果若不同,就照實寫不同。
例如 timeout 被 retry 三次才回應、breaker 沒有打開、log 少了 deployment version,都比「整體成功」更有價值。
RAG workflow 的一個常見錯覺是:retrieval service 正常回 200,就代表 answer 有足夠根據。
空結果、過期 index、權限過濾後空結果,以及結果雖多但都不相關,都是不同 failure class。
今天先從最容易重現的 empty result 開始。
在 retrieval adapter 內加入同樣的 selected-request guard。
def retrieve_documents(question: str, chaos: ChaosContext) -> list[dict]:
if chaos.scenario is ChaosScenario.EMPTY_RETRIEVAL:
return []
return [
{
"document_id": "synthetic-policy-v1",
"title": "Synthetic Remote Work Policy",
"content": "Synthetic content for an isolated lab.",
}
]
workflow 不要把空 list 直接交給 model,期待它「知道自己不知道」。
先寫明確的 policy decision。
def answer_workflow(question: str, chaos: ChaosContext) -> AskResult:
documents = retrieve_documents(question, chaos)
if not documents:
return AskResult(
technical_status="completed",
workflow_status="insufficient_context",
task_status="not_completed",
quality_status="not_grounded",
safety_status="no_tool_action",
degraded=False,
answer=None,
reason="retrieval_empty",
)
# Provider path follows here.
...
這裡的 technical_status="completed" 是一個重要細節。
retriever 可以正常完成查詢,workflow 也可以正常做出「不能回答」的決定。
但使用者的原任務沒有完成。
把這三層狀態混成單一 boolean,dashboard 再漂亮也救不了調查。
result_count=0 是否可在 trace 找到?預期觀測:model span 不應存在,或應清楚標註為 skipped。
預期觀測:output 含 workflow_status=insufficient_context。
預期觀測:citation counter 不會誤把空結果當成有效 citation。
驗收:任何 downstream retry 都不能改變「沒有足夠 context」這個事實。
若你把所有 failure 都重試 provider,empty retrieval 會白白多花 token。
更糟的是,retry 後可能剛好出現一個不相關文件,讓 bug 偶發地被掩蓋。
正確的修復方向可能是 index freshness、query rewrite、document permission 或資料治理。
它不一定是更大的 model。
Agent 或 tool-enabled workflow 的風險不只「回答錯」。
它也可能組出錯誤參數、呼叫過期 schema,或把模型輸出的字串當成已授權 action。
因此,schema mismatch 的實驗不該追求「最後還是呼叫成功」。
它應驗證系統會拒絕未驗證的 action。
先為 tool call 定義一個極小 contract。
from dataclasses import dataclass
@dataclass(frozen=True)
class ToolCall:
name: str
schema_version: str
arguments: dict[str, object]
class ToolSchemaMismatch(Exception):
pass
def validate_tool_call(call: ToolCall, expected_schema: str) -> None:
if call.schema_version != expected_schema:
raise ToolSchemaMismatch(
f"expected={expected_schema}, got={call.schema_version}"
)
再將 chaos scenario 放在 tool planner 或 fake provider,讓它產生舊版 schema。
def plan_tool_call(chaos: ChaosContext) -> ToolCall:
schema_version = "tool-v1"
if chaos.scenario is ChaosScenario.TOOL_SCHEMA_MISMATCH:
schema_version = "tool-v0"
return ToolCall(
name="lookup_policy",
schema_version=schema_version,
arguments={"policy_id": "synthetic-remote-work"},
)
workflow 必須先驗證,才有 dispatch 的資格。
def execute_tool_if_allowed(call: ToolCall) -> dict:
validate_tool_call(call, expected_schema="tool-v1")
return tool_client.execute(call)
這個順序看似無聊。
事故發生時,它會決定你是在處理一筆被拒絕的 request,還是在處理一個真的發出去的錯誤操作。
| 檢查點 | 預期 | 不可接受結果 |
|---|---|---|
| schema validator | 明確拒絕 tool-v0 |
靜默轉換或忽略版本 |
| dispatch counter | 不增加 | 實際呼叫 tool provider |
| trace | 有 policy decision 與 schema version | 只有「tool failed」 |
| response | 要求修正、人工處理或安全拒絕 | 假裝 action 已完成 |
| audit log | 有 request_id、decision、actor scope | 記錄完整敏感 argument |
驗收:將 tool-v0 注入一次後,沒有任何非冪等 action 被重送或猜測成功。
如果 tool 是讀取型,產品可選擇較溫和的 fallback。
如果 tool 是寫入型、付費型或權限型,預設應更保守。
「先試看看」不是 authorization model。
Day 29 已經介紹 timeout、retry、backoff 和 circuit breaker。
今天驗證的是 breaker 會不會在真實 workflow 中做出正確狀態轉換。
先把狀態與門檻明確化。
closed
└─ 連續或比例失敗超過門檻 → open
open
└─ cooldown 到期 → half_open
half_open
├─ 探測成功 → closed
└─ 探測失敗 → open
為什麼中間要多一個 half_open,不能 open 一段時間後就直接跳回 closed?
因為直接跳回 closed 等於用「全部流量」去賭下游是不是已經恢復。如果下游其實還沒好,你等於把剛剛才被 breaker 擋下來的整批流量,一次全部重新打過去。這和完全沒有 breaker 沒有兩樣,只是延後發生。half_open 的作用是先放行少量、可控數量的探測 request,用小規模的代價換取一個訊號:下游到底好了沒。探測失敗就退回 open,不會把整個系統重新暴露在還沒恢復的依賴前面。
不要把「五次 timeout 後 open」當成普世答案。
門檻需要根據流量、錯誤型態、下游恢復時間與產品降級能力決定。
例如低流量服務的五次 failure 可能代表半小時,等太久。
高流量服務的五次 failure 可能只是幾十毫秒的雜訊。
對隔離 Lab,你可以用很小門檻讓狀態容易觀察,但要在 card 註明它不是 production threshold。
from dataclasses import dataclass
@dataclass
class SimpleBreaker:
failure_threshold: int = 2
consecutive_failures: int = 0
state: str = "closed"
def before_call(self) -> None:
if self.state == "open":
raise RuntimeError("circuit_open")
def record_success(self) -> None:
self.consecutive_failures = 0
self.state = "closed"
def record_failure(self) -> None:
self.consecutive_failures += 1
if self.consecutive_failures >= self.failure_threshold:
self.state = "open"
這是教學模型,不處理 clock、half-open、concurrency 或分散式狀態同步。
真正 service 不應直接複製它上 production。
但它足以讓你練習觀察兩次 selected provider timeout 後,第三次 request 是否不再打到 provider。
closed。open。closed。預期觀測:open breaker 降低 downstream call 數與 request latency。
預期觀測:open breaker 不會把 task 失敗偽裝成 completed。
驗收:你能從 evidence 說明每一次狀態轉換的觸發 request,以及第三筆為何沒有呼叫 provider。
這整套狀態機看起來像是在防微杜漸的小題大作,直到你想起上篇 ⑦ 段那個沒有 kill switch 的 Knight Capital。Knight Capital 的系統裡完全沒有這種「異常累積到門檻就自動 open、擋下後續流量」的機制——沒有 breaker,只有一群工程師在異常已經持續好幾十分鐘後,才靠人工介入。SimpleBreaker 這種教學等級的實作,門檻設 2 看起來很隨便,但它體現的原則跟 Knight Capital 缺少的東西完全一樣:系統要有能力在異常持續發生時,自己先擋下後續流量,不必等到人類注意到、看懂、決定該怎麼辦。breaker 慢一秒鐘 open,都比完全沒有 breaker 要好。
如果 service 有多個 process 或 instance,in-memory breaker 可能只在其中一個 worker open。
這不一定錯。
它可能是有意識的 per-instance isolation。
但若你以為它是全域保護,實驗就會揭露不同結果:一半流量繼續打爆下游,另一半快速失敗。
把 scope 寫進 card:per-request、per-process、per-instance、per-region 或 global。
沒有 scope 的 breaker 名稱,和沒有 unit 的 latency 一樣危險。
Cloudflare 在上篇第 ⑦ 段那次中斷的事後報告裡,把「增設更多全域 kill switch」列為修復項目之一。換句話說,他們手上並不缺開關,缺的是一個範圍夠精準、能單獨關掉出問題 feature 的開關,而不必讓整條 proxy 陪著重啟。這正是這裡要驗證的東西:breaker 或 kill switch 的 scope,決定了你事故當下能不能只切掉壞的那一小塊,還是只能選擇「整個服務一起停」或「什麼都不做」這兩個極端。實驗時先問自己一句:如果這個 breaker 真的 open 了,它擋住的是一個 request、一個 provider,還是整個服務?答案往往和你原本以為的不一樣。
AI workflow 常因為 token generation、tool execution 或 GPU queue 產生長尾。
此時只在 HTTP handler 設 timeout 不夠。
request 進 queue 後,worker 仍可能開始處理一件使用者早就離開的工作。
先把 deadline 放進 request context。
from dataclasses import dataclass
from time import monotonic
@dataclass(frozen=True)
class RequestDeadline:
expires_at: float
def expired(self) -> bool:
return monotonic() >= self.expires_at
enqueue 時將 deadline 與 request_id 一起保存。
worker dequeue 時先檢查 deadline。
def process_job(job: dict) -> AskResult:
deadline = RequestDeadline(expires_at=job["expires_at"])
if deadline.expired():
return AskResult(
technical_status="completed",
workflow_status="deadline_exceeded_before_execution",
task_status="not_completed",
quality_status="not_evaluated",
safety_status="no_tool_action",
degraded=False,
answer=None,
reason="queue_delay",
)
return answer_workflow(job["question"], job["chaos"])
這個例子不處理 job acknowledgement、lease 或 exactly-once delivery。
它要傳達的只有一件事:deadline 不能在跨 queue 時蒸發。
在隔離 Lab,可以用 fake worker 在處理前等待固定時間。
from time import sleep
def fake_queue_delay(seconds: float, chaos: ChaosContext) -> None:
if chaos.scenario.value == "queue-delay":
sleep(seconds)
若你採用 async runtime,請用對應的非阻塞 sleep。
本文故意不指定框架,因為核心是 deadline contract,不是 sleep API。
預期觀測:queue age metric 增加。
預期觀測:過期 job 不會啟動 model call 或 write tool。
預期觀測:使用者端看到可理解的逾時結果,或可依產品設計查詢非同步 job 狀態。
驗收:沒有任何 request 在 deadline 過期後仍執行高成本或有副作用的工作。
當 queue 延遲升高,把 timeout 從 30 秒改成 120 秒通常只是延後排隊爆炸。
更多 request 留在記憶體、更多 worker 被卡住、更多使用者同時重試。
有時候可接受的行為就是取消。
這表示服務誠實地遵守 deadline。
Chaos experiment 最容易只盯著 exception。
但安全的 fallback 也可能過期、越權或不適用。
假設你的 provider outage fallback 使用快取的政策摘要。
至少要有下列 metadata。
{
"fallback_source": "policy-summary-cache",
"source_version": "policy-2026-09-01",
"generated_at": "2026-09-01T00:00:00+08:00",
"expires_at": "2026-09-08T00:00:00+08:00",
"audience": "synthetic-lab",
"allowed_intents": ["read_only_policy_question"]
}
實驗要故意放一筆過期 fallback。
預期結果不該是「至少有答案」。
它應該被拒絕,或標示為無法提供最新資訊。
再放一筆 audience 不匹配的 fallback。
它也不應因為 provider 掛了就被放寬存取控制。
這類實驗把 reliability 與 governance 放在同一條路徑。
使用者通常不在乎你是因為 outage 還是 cache 拿到錯資料。
他只會記得答案害他做了錯決定。
若你每次跑 chaos experiment 都 page on-call,團隊很快會討厭這個練習。
若你把實驗期間的所有 alert 靜音,真的事故也可能被一起蓋掉。
兩者都不好。
比較好的作法是讓 alert policy 能識別受控實驗。
例如在隔離環境使用獨立 routing key。
或是將 experiment window 和實驗 ID 寫進 alert annotation,而不是直接 suppress 每一個 dependency failure。
alert: AskProviderTimeoutRateHigh
expr: |
sum(rate(dependency_failures_total{
dependency="model_provider",
failure_class="timeout"
}[5m])) > 0.2
for: 10m
labels:
severity: page
annotations:
runbook: "docs/runbooks/provider-timeout.md"
summary: "Model-provider timeouts exceed the declared threshold"
這只是 PromQL 概念範例。
門檻與 metric 名稱必須符合你的量測定義。
實驗 card 可要求 observer 在開始前確認 alert routing。
預期觀測可能是:Lab alert 會觸發 ticket,但不會呼叫 production on-call。
如果實驗預期不會跨過 alert threshold,也要寫清楚。
「沒有 alert」可能是符合預期,也可能是 alert 壞了。
判斷要靠你預先寫下的門檻,不靠結束後的解釋。
Google 的 emergency response 章節提到過受控測試意外失控時立即 abort 的案例;這正是 abort 條件應在開始前明確化的理由。Emergency Response
一次實驗結束後,整理 evidence packet。
它可以是 Markdown、issue template 或內部系統的 record。
關鍵是資料之間能互相指向。
# ce-ask-provider-timeout-v1 result
## Scope
- Environment: isolated-lab
- Service version: ask-api@abc1234
- Prompt version: policy-answer-v3
- Retrieval index: synthetic-index-v1
## Hypothesis
- Selected provider timeout does not dispatch a tool action.
## Actual
- request_id: req-...
- workflow_status: provider_unavailable
- task_status: not_completed
- tool_action_attempted_delta: 0
## Evidence
- metrics query: ...
- trace_id: ...
- log query: request_id=req-...
- alert status: not expected to fire in this run
## Decision
- Passed with one gap: breaker state lacks a metric.
## Follow-up
- Owner: ...
- Due condition: before enabling provider fallback in staging
不要把 dashboard screenshot 當成唯一 evidence。
截圖沒有 query、時間窗、filter 和版本時,很難重新判讀。
至少保留 query、trace ID、log filter、request list 與資料版本。
也不要把「passed」當成實驗輸出本身。
它是根據 evidence 作出的 decision。
如果 card 的 hypothesis 只驗證了 fake provider,evidence 不能外推成「真實供應商 outage 已被證明安全」。
這是驗證邊界,不是謙虛用語。
| 結果 | 可以說什麼 | 不可以說什麼 | 下一步 |
|---|---|---|---|
| 通過 | 在這個環境、版本、範圍內,觀察結果符合假設 | production 永遠安全 | 將關鍵路徑轉為 regression test 或擴大一個小範圍 |
| 假設被推翻 | 某個 contract 沒被滿足 | chaos engineering 沒用 | 修正 failure handling,保留案例 |
| 結果不明 | evidence 不足以判定 | 應該算通過 | 補 telemetry、縮小變因後重跑 |
| 中止 | abort condition 被觸發,實驗停止 | 已驗證或失敗 | 先處理 blast radius/guardrail 缺口 |
「中止」不一定是負面結果。
若 abort 機制如預期阻止擴散,它本身是有用 evidence。
但被中止的主假設仍然沒有完成驗證。
不要在 report 裡把兩件事混在一起。
每一個 experiment 至少選一個後續動作。
| 發現 | 合適的後續動作 |
|---|---|
| provider timeout 被誤算 task success | 加入 outcome classification regression test,更新 SLI query |
| empty retrieval 仍會呼叫 model | 在 workflow 加 early return,建立 dataset case |
| schema mismatch 仍 dispatch tool | 增加 server-side validator,收緊 permission boundary |
| request_id 無法串起 log 和 trace | 修正 context propagation,補 runbook |
| breaker open 不可觀察 | 新增狀態 metric 和 dashboard panel |
| fallback 使用過期快取 | 版本化 cache metadata,加入 freshness gate |
| 實驗造成真實 alert routing | 分隔 Lab 與 production route,更新 abort check |
Day 27 的 postmortem 強調 action item 要有 owner 與可驗證的完成條件。
Chaos experiment 的 follow-up 也一樣。
「改善韌性」不是 action item。
「在 answer_workflow 對 empty retrieval 早退,並以 synthetic case 驗證 model span 沒產生」才是。
Day 28 的 change manifest 也應列出這些行為變更。
修 provider fallback 可能改變使用者可見的結果,不能當作純 internal refactor。
以下是讀者可以自行調整的練習節奏。
它不是 production runbook。
它的好處是避免「準備 50 分鐘、注入 5 秒鐘、結論卻花一週猜」的常見狀況。
| 時間 | 動作 | 產出 |
|---|---|---|
| 0–10 分 | 選單一假設,填 card | hypothesis、scope、abort |
| 10–25 分 | 檢查 baseline 與 observability | baseline request_id、dashboard query |
| 25–35 分 | 演練 rollback | 可重述的 stop procedure |
| 35–50 分 | 注入一個 scenario | selected request evidence |
| 50–65 分 | 查 metrics、logs、traces | actual outcome |
| 65–75 分 | 停用 injection,重跑 baseline | recovery evidence |
| 75–90 分 | 寫 decision 與 follow-up | evidence packet、owner |
一開始不要在 90 分鐘內同時測 provider、DB、queue、tool、GPU 及 evaluator。
那是一場故障拼盤。
最好的第一個實驗通常很小:一筆合成 request、一個 fake timeout、一條清楚的 task-status contract。
它若失敗,修起來快。
它若通過,也能成為下一個實驗的 baseline。
[ ] 已指定隔離環境與合成資料範圍
[ ] 已定義一個可否證 hypothesis
[ ] 已確認 steady state,且沒有既有 incident
[ ] injection 只能由明確 selector 啟用
[ ] 有 max duration 與 max request 限制
[ ] 已寫 abort condition 與實際 rollback 動作
[ ] observer 知道要查哪些 metrics、logs、traces
[ ] request_id 不會進入 Prometheus 高 cardinality label
[ ] 已保護 prompt、文件、credential 與個資
[ ] 已指定是否允許 tool,以及預設拒絕行為
[ ] 已確認 alert routing 不會誤 page 不相關 on-call
[ ] 已準備 experiment card 與 evidence packet 位置
[ ] 已記錄實際開始與結束時間
[ ] 已保存 selected request_id 與版本資訊
[ ] 已比對 expected 和 actual outcome
[ ] 已確認 tool action delta
[ ] 已確認 metrics、logs、traces 的因果關係
[ ] 已執行 rollback,並取得 baseline recovery evidence
[ ] 已標示結果為 passed、failed、inconclusive 或 aborted
[ ] 已寫明驗證邊界,沒有外推到未測環境
[ ] 已建立具 owner 與完成條件的 follow-up
[ ] 已將可重現 failure 放回 regression suite 或 dataset
結果通常是一堆 log。
但你不知道哪一筆是實驗造成,哪一筆是原本就存在的雜訊。
修法:先寫 hypothesis 和 observe 欄位,再開始任何 injection。
provider timeout 加 DB disconnect 加 queue delay,確實很像大型事故。
但第一次做時,你沒有辦法知道是哪個 guardrail 失效。
修法:一次一個 primary fault;後續再設計組合 scenario。
Pod 活著、CPU 不高、HTTP 200,都可能和使用者任務失敗並存。
修法:同時寫 technical、workflow、quality、safety、task outcome。
如果 retry 後 request 成功,可能代表系統韌性良好。
也可能代表你根本沒有驗證 retry 上限、成本與 deadline。
修法:記錄 attempt count、backoff、總 latency、token 或外部 call 數。
這通常表示 fake state、cache 或 breaker 還留著。
修法:recovery baseline 是每張 card 的必填 acceptance criterion。
過兩週換了 prompt、模型或 retriever,這個結論已經很難解釋。
修法:記錄 code、prompt、model、index、evaluator 與 experiment revision。
production chaos 需要成熟的 SLO、隔離、observability、on-call 協調與回退能力。
新系統先在 Lab 做得小且可重跑,比直接放大 blast radius 更有工程價值。
Netflix 對 chaos engineering 的原始論文強調它是建立系統承受真實擾動信心的實驗方法,不是追求破壞性的表演。Chaos Engineering
| 可靠性層 | 可注入的假設 | 可觀察的證據 | 安全限制 |
|---|---|---|---|
| Infrastructure | GPU worker 變慢 | utilization、queue age、deadline | 不對共用 production node 注入 |
| Service | /ask timeout |
request outcome、latency、error | synthetic request selector |
| Dependency | provider 或 vector DB timeout | dependency span、retry、breaker | fake adapter 或隔離 endpoint |
| Workflow | parser 或 router 失敗 | step outcome、fallback decision | 不繞過 validator |
| Semantic | context 不足 | groundedness、citation、task status | 不生成無根據答案 |
| Economic | retry amplification | attempts、token、queue cost | 設 request 上限 |
| Governance | schema/permission mismatch | policy decision、audit event | 預設 deny、無寫入 action |
這張表不是要你在一天內把七層都測完。
它用來避免一個盲點:AI reliability 不是只多了一個 model provider。
你在 Day 30 驗證的,是跨層承諾是否能在 failure 中保持一致。
先做 provider timeout。
使用 fake adapter、synthetic request 和一個 outcome counter。
目標是練習「失敗不等於 task completed」與 request correlation。
再做 empty retrieval 和 index version。
目標是讓 insufficient_context 成為可量測的正常結果,而不是 error bucket 的垃圾桶。
優先做 schema mismatch、permission denied 和 deadline exceeded。
目標不是維持 agent 看起來很會做事。
目標是確保它失敗時不會執行未授權 action。
把實驗結果連到 Day 27 postmortem、Day 28 manifest、Day 29 degradation policy 和 Day 31 的 regression dataset。
目標是讓每次失敗都留下可被下一次 release 重跑的 evidence。
以下清單是本文的讀者驗收條件。
[ ] 能清楚區分 unit test、integration test、load test 與 chaos experiment
[ ] 已寫出一個可否證 hypothesis,而不是「系統很有韌性」
[ ] 已定義 steady state、blast radius、abort 和 rollback
[ ] 已將注入限制在 selected synthetic request
[ ] 已設計 provider timeout、empty retrieval 或 tool schema mismatch 其中一個 scenario
[ ] 已為 scenario 定義 technical、workflow、quality、safety、task outcome
[ ] 已確認 request_id 可串起 log、trace、metrics 關聯與 evidence record
[ ] 沒有把 request_id 放入高 cardinality metric label
[ ] 已確認 fallback 不會回答沒有根據的內容,也不會繞過 tool policy
[ ] 已驗證或設計 breaker、deadline、retry 的可觀察行為
[ ] 已執行或規劃 recovery baseline,不把實驗殘留當成正常狀態
[ ] 已留下 experiment card、actual result 與具 owner 的 follow-up
[ ] 已寫明隔離 Lab 的結果不能外推成 production 已驗證
這個系列從 Day 1 的 trace 開始。
當時我們只是在看一個 request 穿過 prompt、retrieval 和 LLM 後發生了什麼。
Day 30 的問題更嚴格。
當其中一段壞掉時,系統還會不會誠實地描述狀態、保護使用者、留下證據並回到可預期的 baseline?
答案不會是一個 dashboard、一個 framework 或某次 demo。
它是一連串明確的工程選擇:定義 SLI、保留 error budget、分開 task success 與 HTTP success、限制 retry、拒絕越權 tool、記錄版本、把失敗轉成 regression case。
Chaos experiment 的角色,是把「我們相信」改成「我們在這個範圍內測到」。
這句話沒有那麼熱血。
但它能讓下一次 incident 少一點猜測。
Day 31 特別篇會把這些 trace、failure、dataset、evaluation 與 release decision 接成一個 AI reliability learning loop。
工具可以換。
可靠性證據鏈不要斷。
這篇是 Learning SRE for the AI Era 系列的一部分:從 SRE Lab 到 Production AI Reliability。
Build → Trace → Break → Measure → Evaluate → Recover → Improve.